<?php
/**
 * Created by PhpStorm.
 * User: yangliqiang
 * Date: 2018/8/11
 * Time: 14:40
 * auth:基础类库（微信公众号）
 */

namespace console\core\wechat;
use Yii;
use Cm;
use console\core\CreateUuid;
class wechat
{
    /**
     * 授权方面的地址
     */
    const API_URL_PREFIX = 'https://api.weixin.qq.com/cgi-bin';
    const API_COMPONENT_TOKEN_URL = '/component/api_component_token';
    const API_CREATE_PREAUTHCODE_URL = '/component/api_create_preauthcode?component_access_token=';
    const API_QUERY_AUTH_URL = '/component/api_query_auth?component_access_token=';
    const API_AUTHORIZER_TOKEN_URL = '/component/api_authorizer_token?component_access_token=';
    const API_GET_AUTHORIZER_INFO_URL = '/component/api_get_authorizer_info?component_access_token=';
    const API_GET_AUTHORIZER_OPTION_URL = '/component/api_get_authorizer_option?component_access_token=';
    const API_SET_AUTHORIZER_OPTION_URL = '/component/api_set_authorizer_option?component_access_token=';
    const API_REDIRECT = 'https://mp.weixin.qq.com/cgi-bin/componentloginpage?';


    /**
     * @var
     * #微信第三平台appid
     */
    public static $appid;
    /**
     * @var
     * #微信第三平台appid
     */
    public static $appsecret;

    /**
     * 第三方平台消息加密的token
     * @var
     */
    public static $token;
    /**
     *  第三方平台消息加密的encodingAesKey
     * @var
     */
    public static $encodingAesKey;
    /**
     * 微信第三方ticket值
     * @var
     */
    private $component_verify_ticket;

    /**
     * @var
     * 微信第三方平台token
     */
    private $component_access_token;

    /**
     * @var
     * #授权微信token
     */
    public $authorizer_access_token;

    /**
     * 更新授权公众号的token
     * @var
     */
    public static $authorizer_refresh_token;

    /**
     * 请求第三方$component_access_token
     * @var array
     */
    public $options = [];

    /**
     * 错误日志
     * @var string
     */
    public $error = '';

    /**
     * 微信错误码
     * @var int
     */
    public $errCode = 40001;

    /**
     * 错误日志
     * @var string
     */
    public $errMsg = "no access";

    /**
     * 授权微信公总号在数据里边的id
     * @var int
     */
    public $wx_app_data_id = 0;

    /**
     * 公众号关注的用户id：
     * @var int
     */
    public $company_id = 0;


    /**
     * 初始化操作
     * wecaht constructor.
     */
    public function __construct()
    {
        //以下三个变量，自己去开放平台上管理中心根据实际情况填写。
        $encodingAesKey = Yii::$app->params['wechat']['encodingAesKey'];
        $token = Yii::$app->params['wechat']['token'];
        $appId = Yii::$app->params['wechat']['appid'];

        //获取数据
        $ticket = Cm::$db->one("SELECT appid,appsecret,encodingAesKey,component_verify_ticket,token FROM `scrm_weixin_three_account` WHERE appid='{$appId}'");
        if (!$ticket) {
            new \Exception('component_verify_ticket不存在！');
        }

        self::$appid = $appId;
        self::$appsecret = $ticket['appsecret'];
        self::$token = isset($ticket['token']) && !empty($ticket['token']) ? $ticket['token'] : $token;
        self::$encodingAesKey = isset($ticket['encodingAesKey']) && !empty($ticket['encodingAesKey']) ? $ticket['encodingAesKey'] : $encodingAesKey;

        //$this->component_verify_ticket = substr($ticket['component_verify_ticket'], strlen('ticket@@@'));
        $this->component_verify_ticket = $ticket['component_verify_ticket'];

        $this->options = [
            'component_appid' => self::$appid,
            'component_appsecret' => self::$appsecret,
            'component_verify_ticket'=>$this->component_verify_ticket
        ];

    }



    /**
     * 获取第三方平台令牌（component_access_token）
     * 数据实例：{"component_appid":"appid_value" , "component_appsecret": "appsecret_value",  "component_verify_ticket": "ticket_value"   }
     * @return bool
     */
    public function getComponentAccessToken()
    {

        $cache =Cm::$db->one(
            "SELECT component_access_token, component_access_token_expires  FROM `scrm_weixin_three_account` WHERE appid='".self::$appid."'"
        );
        if ($cache && $cache['component_access_token_expires'] > time()) {
            $this -> component_access_token = $cache['component_access_token'];
            return $cache['component_access_token'];
        }
        $result = $this -> http_post(self::API_URL_PREFIX . self::API_COMPONENT_TOKEN_URL, json_encode($this -> options));

        if ($result) {
            $json = json_decode($result, true);
            if (!$json || !empty($json['errcode'])) {
                $this -> errCode = $json['errcode'];
                $this -> errMsg = $json['errmsg'];
                return false;
            }
            $this -> component_access_token = $json['component_access_token'];
            //写入文件来设置缓存
            if (!$cache) {
                Cm::$db->insert('scrm_weixin_three_account', [
                    'id'=>(new CreateUuid())->toString(),
                    'component_access_token'=>$this->component_access_token,
                    'appid'=>self::$appid,
                    'component_access_token_expires'=>time()+7100
                ]);
            } else {
                Cm::$db->update('scrm_weixin_three_account', [
                    'component_access_token'=>$this->component_access_token,
                    'component_access_token_expires'=>time()+7100
                ], "appid='".self::$appid."'");
            }
            return $this -> component_access_token;
        } else {
        }
        return false;
    }

    /**
     * 获取（刷新）授权公众号的令牌
     * 该API用于在授权方令牌（authorizer_access_token）失效时，可用刷新令牌（authorizer_refresh_token）获取新的令牌。
     * @param $authorizer_appid
     * @param $authorizer_refresh_token
     * @return bool
     */
    public function getAuthorizerRefreshToken($authorizer_appid, $authorizer_refresh_token)
    {
        $component_access_token = $this -> getComponentAccessToken();
        if (!$component_access_token) {
            return false;
        }
        $response = [
            'component_appid' => self::$appid,
            'authorizer_appid' => $authorizer_appid,
            'authorizer_refresh_token' => $authorizer_refresh_token
        ];

        $result = $this -> http_post(self::API_URL_PREFIX . self::API_AUTHORIZER_TOKEN_URL . $component_access_token, json_encode($response));
        if ($result) {
            $json = json_decode($result, true);
            if (!$json || !empty($json['errcode'])) {
                $this -> errCode = $json['errcode'];
                $this -> errMsg = $json['errmsg'];
                return false;
            }

            $data = Cm::$db->one("SELECT id, authorizer_access_token,authorizer_access_token_time,authorizer_refresh_token FROM `scrm_weixin_account` WHERE accountappid='{$authorizer_appid}'");

            if (!$data) {
                $this->wx_app_data_id = (new CreateUuid())->toString();
                Cm::$db->insert('scrm_weixin_account', [
                    'id'=>$this->wx_app_data_id,
                    'accountappid'=>$json['authorizer_appid'],
                    'authorizer_access_token'=>$json['authorizer_access_token'],
                    'authorizer_access_token_time'=>time()+7100,
                    'authorizer_refresh_token'=>$authorizer_refresh_token,
                ]);
            } else {
                $this->wx_app_data_id  = $data['id'];
                Cm::$db->update('scrm_weixin_account', [
                    'authorizer_access_token'=>$json['authorizer_access_token'],
                    'authorizer_access_token_time'=>time()+(int)$json['expires_in'],
                ], "accountappid='{$authorizer_appid}'");
            }
            $this -> authorizer_access_token = $json['authorizer_access_token'];

            return $json['authorizer_access_token'];
        }
        return false;
    }

    /**
     * 获取授权公众号的authorizer_access_token
     * @param $authorizer_appid
     * @return bool
     */
    public function getAuthorizerAccessToken($authorizer_appid)
    {

        $cache = Cm::$db->one("SELECT id, authorizer_access_token,authorizer_access_token_time,authorizer_refresh_token FROM `scrm_weixin_account` WHERE accountappid='{$authorizer_appid}'");
        $authorizer_access_token = '';
        if ($cache) {
            $this->wx_app_data_id = $cache['id'];
            if ((int)$cache['authorizer_access_token_time'] > time()) {
                $authorizer_access_token = $cache['authorizer_access_token'];
            } else {
                $authorizer_refresh_token = $cache['authorizer_refresh_token'];

                $authorizer_access_token = $this -> getAuthorizerRefreshToken($authorizer_appid, $authorizer_refresh_token);

            }
        }
        $this->getCompanyId($this->wx_app_data_id);

        $this->authorizer_access_token = $authorizer_access_token;

        return $this->authorizer_access_token;
    }

    /**
     * 权限分配：
     * @param $appid
     * @param $fun
     */
    public function getAuthInfo($authorizer_appid, $fun=0)
    {

        $cache = Cm::$db->one("SELECT status, func_info,accountverifytype FROM `scrm_weixin_account` WHERE accountappid='{$authorizer_appid}'");

        if (!$cache) {
            $this->error = "公众号appi:{$authorizer_appid}未找到！";
            return false;
        } else {
            if ((int)$cache['status']===0) {
                $this->error = "公众号appi:{$authorizer_appid}未授权或已取消授权！";
                return false;
            }
            /**
             * 1、微博认证视作未认证,因此微博认证的公众号不会拥有微信认证公众号特有的接口。
             * 2、微信认证分为资质认证和名称认证两部分，只需要资质认证通过，就可获得接口。
             * $verifyType:0未认证， 1已认证
             */
            $verifyType = in_array((int)$cache['accountverifytype'], [-1, 1, 2]) ? 0 : 1;
            if ((int)$verifyType===0) {
                $this->error = "公众号appi:{$authorizer_appid}未认证！";
                return false;
            }
            $funInfo = explode(',', $cache['func_info']);
            if ( $fun && !in_array($fun, $funInfo)) {
                $this->error = "公众号appi:{$authorizer_appid}未授权！";
                return false;
            }
        }
        return true;
    }

    /**
     * 获取公司id：
     * @param $wx_account_id
     * @return int
     */
    public function getCompanyId($wx_account_id)
    {
        $cache = Cm::$db->one("SELECT company_id FROM `scrm_weixin_company_account` WHERE wx_account_id='{$wx_account_id}'");
        if (!$cache && isset($cache['company_id'])) {
            $this->company_id = $cache['company_id'];
        }

        return $this->company_id;
    }

    /**
     * 记录日志
     */
    public function __destruct()
    {
        if (!empty($this->error)) {
            $msg = !empty($this->errCode) ? "，微信错误码：{$this->errCode}错误日志:{$this->errMsg}" : '';
            YII_DEBUG && self::logResult('/log-debug.log','详细错误：'.$this->error.$msg);
        }
    }

    /**
     * 写日志
     * @param $path
     * @param $data
     */
    public static function logResult($path, $data)
    {

        if (!is_dir(LOGPATH)) {
            mkdir(LOGPATH, 0777, true);
        }

        file_put_contents(LOGPATH.$path, '[时间：'.date('Y-m-d  H:i:s',time()).']，结果：'.$data."\r\n",FILE_APPEND);
    }


    /**
     * GET 请求
     * @param string $url
     */
    public function http_get($url)
    {
        $oCurl = curl_init();
        if (stripos($url, "https://") !== FALSE) {
            curl_setopt($oCurl, CURLOPT_SSL_VERIFYPEER, FALSE);
            curl_setopt($oCurl, CURLOPT_SSL_VERIFYHOST, FALSE);
        }
        curl_setopt($oCurl, CURLOPT_URL, $url);
        curl_setopt($oCurl, CURLOPT_RETURNTRANSFER, 1);
        $sContent = curl_exec($oCurl);
        $aStatus = curl_getinfo($oCurl);
        curl_close($oCurl);
        if (intval($aStatus["http_code"]) == 200) {
            return $sContent;
        } else {
            return false;
        }
    }

    /**
     * POST 请求
     * @param string $url
     * @param array $param
     * @return string content
     */
    public function http_post($url, $param)
    {
        $oCurl = curl_init();
        if (stripos($url, "https://") !== FALSE) {
            curl_setopt($oCurl, CURLOPT_SSL_VERIFYPEER, FALSE);
            curl_setopt($oCurl, CURLOPT_SSL_VERIFYHOST, false);
        }
        if (is_string($param)) {
            $strPOST = $param;
        } else {
            $aPOST = array();
            foreach ($param as $key => $val) {
                $aPOST[] = $key . "=" . urlencode($val);
            }
            $strPOST = join("&", $aPOST);
        }
        //file_put_contents('./mylog.txt',$url."\r\n".$strPOST."\r\n".date('Y-m-d H:i:s')."\r\n==============\r\n",FILE_APPEND);
        curl_setopt($oCurl, CURLOPT_URL, $url);
        curl_setopt($oCurl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($oCurl, CURLOPT_POST, true);
        curl_setopt($oCurl, CURLOPT_POSTFIELDS, $strPOST);
        $sContent = curl_exec($oCurl);
        $aStatus = curl_getinfo($oCurl);
        curl_close($oCurl);
        if (intval($aStatus["http_code"]) == 200) {
            return $sContent;
        } else {
            return false;
        }
    }

    /**
     * @param $url
     * @param $param
     * @return bool|mixed
     */
    public function http_post_file($url, $param)
    {
        $oCurl = curl_init();

        if (class_exists('\CURLFile')) {
            curl_setopt($oCurl, CURLOPT_SAFE_UPLOAD, true);
        } else {
            if (defined('CURLOPT_SAFE_UPLOAD')) {
                curl_setopt($oCurl, CURLOPT_SAFE_UPLOAD, false);
            }
        }


            if (stripos($url, "https://") !== FALSE) {
            curl_setopt($oCurl, CURLOPT_SSL_VERIFYPEER, FALSE);
            curl_setopt($oCurl, CURLOPT_SSL_VERIFYHOST, false);
        }
        $strPOST = $param;
//        if (is_string($param)) {
//            $strPOST = $param;
//        } else {
//            $aPOST = array();
//            var_export($param);
//            foreach ($param as $key => $val) {
//
//                $aPOST[] = $key . "=" . urlencode($val);
//            }
//            $strPOST = join("&", $aPOST);
//        }
        //var_export($strPOST);exit;
        //file_put_contents('./mylog.txt',$url."\r\n".$strPOST."\r\n".date('Y-m-d H:i:s')."\r\n==============\r\n",FILE_APPEND);
        curl_setopt($oCurl, CURLOPT_URL, $url);
        curl_setopt($oCurl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($oCurl, CURLOPT_POST, true);
        curl_setopt($oCurl, CURLOPT_POSTFIELDS, $strPOST);
        $sContent = curl_exec($oCurl);
        $aStatus = curl_getinfo($oCurl);
        curl_close($oCurl);
        if (intval($aStatus["http_code"]) == 200) {
            return $sContent;
        } else {
            return false;
        }
    }
    /**
     * 微信api不支持中文转义的json结构
     * @param array $arr
     */
    static function json_encode($arr) {
        $parts = array();
        $is_list = false;
        //Find out if the given array is a numerical array
        $keys = array_keys($arr);
        $max_length = count($arr) - 1;
        if (($keys[0] === 0) && ($keys[$max_length] === $max_length)) {//See if the first key is 0 and last key is length - 1
            $is_list = true;
            for ($i = 0; $i < count($keys); $i++) {//See if each key correspondes to its position
                if ($i != $keys[$i]) {//A key fails at position check.
                    $is_list = false;
                    //It is an associative array.
                    break;
                }
            }
        }
        foreach ($arr as $key => $value) {
            if (is_array($value)) {//Custom handling for arrays
                if ($is_list)
                    $parts[] = self::json_encode($value);
                /* :RECURSION: */
                else
                    $parts[] = '"' . $key . '":' . self::json_encode($value);
                /* :RECURSION: */
            } else {
                $str = '';
                if (!$is_list)
                    $str = '"' . $key . '":';
                //Custom handling for multiple data types
                if (is_numeric($value) && $value < 2000000000)
                    $str .= $value;
                //Numbers
                elseif ($value === false)
                    $str .= 'false';
                //The booleans
                elseif ($value === true)
                    $str .= 'true';
                else
                    $str .= '"' . addslashes($value) . '"';
                //All other things
                // :TODO: Is there any more datatype we should be in the lookout for? (Object?)
                $parts[] = $str;
            }
        }
        $json = implode(',', $parts);
        if ($is_list)
            return '[' . $json . ']';
        //Return numerical JSON
        return '{' . $json . '}';
        //Return associative JSON

    }

    /**
     * [arrayToXml 数组转换成xml]
     * @param  array  $params [description]
     * @return [type]         [description]
     */
    public static function arrayToXml(array $params)
    {
        $xml = "<xml>";

        foreach ($params as $key => $value) {
            if (is_numeric($value)) {
                $xml .= "<{$key}>{$value}</{$key}>";
            } else {
                $xml .= "<{$key}><![CDATA[".$value."]]></{$key}>";
            }
        }

        return $xml.'</xml>';
    }

    /**
     * XML编码
     * @param mixed $data 数据
     * @param string $root 根节点名
     * @param string $item 数字索引的子节点名
     * @param string $attr 根节点属性
     * @param string $id   数字索引子节点key转换的属性名
     * @param string $encoding 数据编码
     * @return string
     */
    public static function xml_encode($data, $root = 'xml', $item = 'item', $attr = '', $id = 'id', $encoding = 'utf-8') {
        if (is_array($attr)) {
            $_attr = array();
            foreach ($attr as $key => $value) {
                $_attr[] = "{$key}=\"{$value}\"";
            }
            $attr = implode(' ', $_attr);
        }
        $attr = trim($attr);
        $attr = empty($attr) ? '' : " {$attr}";
        $xml = "<{$root}{$attr}>";
        $xml .= self::data_to_xml($data, $item, $id);
        $xml .= "</{$root}>";
        return $xml;
    }
    /**
     * 数据XML编码:数组转换成xml
     * @param mixed $data 数据
     * @return string
     */
    public static function data_to_xml($data) {
        $xml = '';
        foreach ($data as $key => $val) {
            is_numeric($key) && $key = "item id=\"$key\"";
            $xml .= "<$key>";
            $xml .= (is_array($val) || is_object($val)) ? self::data_to_xml($val) : self::xmlSafeStr($val);
            list($key, ) = explode(' ', $key);
            $xml .= "</$key>";
        }
        return $xml;
    }

    public static function xmlSafeStr($str) {
        return '<![CDATA[' . preg_replace("/[\\x00-\\x08\\x0b-\\x0c\\x0e-\\x1f]/", '', $str) . ']]>';
    }

    /** xml转换成数组*/
    public static function xmlToArray($xml)
    {
        if (!$xml)
            return false;

        //禁止引用外部xml
        libxml_disable_entity_loader(true);
        //使用SimpleXMLElement转换json成字符串
        $data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
        return $data;
    }

    /**
     * 解密消息并转换成数组
     * @param string $xml
     * @return bool|mixed
     */
    public static function cryptXmlToArray($xml = '' )
    {
        $encodingAesKey = Yii::$app->params['wechat']['encodingAesKey'];
        $token = Yii::$app->params['wechat']['token'];
        $appId = Yii::$app->params['wechat']['appid'];

        if (!$xml)
            $xml = file_get_contents("php://input");

        $timeStamp  = empty($_GET['timestamp'])     ? ""    : trim($_GET['timestamp']) ;
        $nonce      = empty($_GET['nonce'])     ? ""    : trim($_GET['nonce']) ;
        $msg_sign   = empty($_GET['msg_signature']) ? ""    : trim($_GET['msg_signature']) ;
        $encryptMsg = file_get_contents('php://input');
        $pc = new \WXBizMsgCrypt($token, $encodingAesKey, $appId);

        $xml_tree = new \DOMDocument();
        $xml_tree->loadXML($encryptMsg);
        $array_e = $xml_tree->getElementsByTagName('Encrypt');
        $encrypt = $array_e->item(0)->nodeValue;

        $format = "<xml><ToUserName><![CDATA[toUser]]></ToUserName><Encrypt><![CDATA[%s]]></Encrypt></xml>";
        $from_xml = sprintf($format, $encrypt);

        // 第三方收到公众号平台发送的消息
        $msg = '';
        $errCode = $pc->decryptMsg($msg_sign, $timeStamp, $nonce, $from_xml, $msg);

        if ($errCode == 0) {
            YII_DEBUG && self::logResult('/result.log','解密后文件：'.$msg);

            return self::xmlToArray($msg);
        } else {
            YII_DEBUG && self::logResult('/error.log','解密后失败：'.$errCode);
            return false;
            //print($errCode . "\n");
        }
    }

    /**加密消息体：并转换成xml
     * @param $data
     * @param bool $json
     * @return bool|string
     */
    public static function cryptArrayToXml($data, $nonce='', $json = false)
    {

        $encodingAesKey = Yii::$app->params['wechat']['encodingAesKey'];
        $token = Yii::$app->params['wechat']['token'];
        $appId = Yii::$app->params['wechat']['appid'];
        if (!$data) return false;

        if (is_array($data)) {
            if ($json) {
                return json_encode($data);
            }
            $data = self::xml_encode($data);
        }
        $timestamp = time();
        $nonce = !empty($nonce) ? $nonce : self::makeNonceStr();

        $pc = new \WXBizMsgCrypt($token, $encodingAesKey, $appId);
        $encryptMsg = '';

        $errCode = $pc->encryptMsg($data, $timestamp, $nonce, $encryptMsg);
        if ($errCode == 0) {
            return $encryptMsg;
        }
        return false;
    }

    /**
     * 生成制定长度随机数
     * @param  integer $len [指定长度]
     * @return [randChars]       [随机字符串]
     */
    private static function makeNonceStr($len = 32)
    {
        $chars = [
            "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k",
            "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v",
            "w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G",
            "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R",
            "S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2",
            "3", "4", "5", "6", "7", "8", "9"
        ];

        $charsLen = count($chars)-1;
        //打乱数组
        shuffle($chars);

        $randChars = '';
        for ($i=0; $i < $len ; $i++) {
            $randChars .= $chars[mt_rand(0, $charsLen)];
        }

        return $randChars;

    }

    function get_ip() {
        $realip= '未知';
        if(isset($_SERVER)){
            if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
                $realip=$_SERVER['HTTP_X_FORWARDED_FOR'];
            }
            else if(isset($_SERVER['HTTP_CLIENT_IP'])){
                $realip=$_SERVER['HTTP_CLIENT_IP'];
            }
            else{
                $realip=$_SERVER['REMOTE_ADDR'];
            }
        }
        else{
            if(getenv('HTTP_X_FORWARDED_FOR')){
                $realip=getenv('HTTP_X_FORWARDED_FOR');
            }
            else if(getenv('HTTP_CLIENT_IP')){
                $realip=getenv('HTTP_CLIENT_IP');
            }
            else{
                $realip=getenv('REMOTE_ADDR');
            }
        }
        return $realip;
    }
}